// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';

function parseSinePath(pathStr) {
  const points = [];
  const commands = pathStr.match(/[MCL]\s*[\d\s.\-,]+/g);
  if (!commands) return points;

  commands.forEach((command) => {
    const type = command.charAt(0);
    const coordsStr = command.slice(1).trim();
    const coords = coordsStr.split(/[\s,]+/).map(Number);

    switch (type) {
      case 'M':
        points.push({ x: coords[0], y: coords[1], type: 'M' });
        break;
      case 'C':
        if (coords.length >= 6 && coords.length % 6 === 0) {
          for (let i = 0; i < coords.length; i += 6) {
            points.push({
              x1: coords[i],
              y1: coords[i + 1],
              x2: coords[i + 2],
              y2: coords[i + 3],
              x: coords[i + 4],
              y: coords[i + 5],
              type: 'C',
            });
          }
        }
        break;
      case 'L':
        points.push({ x: coords[0], y: coords[1], type: 'L' });
        break;
    }
  });
  return points;
}

const MAX_OPACITY = 0.9;
const MIN_OPACITY = AppConstants.platform === 'macosx' ? 0.25 : 0.35;

const EXPLICIT_LIGHTNESS_TYPE = 'explicit-lightness';
const EXPLICIT_BLACKWHITE_TYPE = 'explicit-black-white';

export class nsZenThemePicker extends nsZenMultiWindowFeature {
  static MAX_DOTS = 3;

  currentOpacity = 0.5;
  dots = [];
  useAlgo = '';
  #currentLightness = 50;

  #allowTransparencyOnSidebar = Services.prefs.getBoolPref('zen.theme.acrylic-elements', false);

  #linePath = `M 51.373 27.395 L 367.037 27.395`;
  #sinePath = `M 51.373 27.395 C 60.14 -8.503 68.906 -8.503 77.671 27.395 C 86.438 63.293 95.205 63.293 103.971 27.395 C 112.738 -8.503 121.504 -8.503 130.271 27.395 C 139.037 63.293 147.803 63.293 156.57 27.395 C 165.335 -8.503 174.101 -8.503 182.868 27.395 C 191.634 63.293 200.4 63.293 209.167 27.395 C 217.933 -8.503 226.7 -8.503 235.467 27.395 C 244.233 63.293 252.999 63.293 261.765 27.395 C 270.531 -8.503 279.297 -8.503 288.064 27.395 C 296.83 63.293 305.596 63.293 314.363 27.395 C 323.13 -8.503 331.896 -8.503 340.662 27.395 M 314.438 27.395 C 323.204 -8.503 331.97 -8.503 340.737 27.395 C 349.503 63.293 358.27 63.293 367.037 27.395`;

  #sinePoints = parseSinePath(this.#sinePath);

  #colorPage = 0;
  #gradientsCache = new Map();

  constructor() {
    super();
    if (!gZenWorkspaces.shouldHaveWorkspaces || gZenWorkspaces.privateWindowOrDisabled) {
      return;
    }
    this.promiseInitialized = new Promise((resolve) => {
      this._resolveInitialized = resolve;
    });
    this.dragStartPosition = null;

    this.isLegacyVersion = Services.prefs.getIntPref('zen.theme.gradient-legacy-version', 1) === 0;

    ChromeUtils.defineLazyGetter(this, 'panel', () =>
      document.getElementById('PanelUI-zen-gradient-generator')
    );
    ChromeUtils.defineLazyGetter(this, 'toolbox', () => document.getElementById('TabsToolbar'));
    ChromeUtils.defineLazyGetter(this, 'customColorInput', () =>
      document.getElementById('PanelUI-zen-gradient-generator-custom-input')
    );
    ChromeUtils.defineLazyGetter(this, 'customColorList', () =>
      document.getElementById('PanelUI-zen-gradient-generator-custom-list')
    );

    ChromeUtils.defineLazyGetter(this, 'sliderWavePath', () =>
      document.getElementById('PanelUI-zen-gradient-slider-wave').querySelector('path')
    );

    this.panel.addEventListener('popupshowing', this.handlePanelOpen.bind(this));
    this.panel.addEventListener('popuphidden', this.handlePanelClose.bind(this));
    this.panel.addEventListener('command', this.handlePanelCommand.bind(this));

    document
      .getElementById('PanelUI-zen-gradient-generator-opacity')
      .addEventListener('input', this.onOpacityChange.bind(this));

    // Call the rest of the initialization
    this.initContextMenu();
    this.initPredefinedColors();

    this._resolveInitialized();
    delete this._resolveInitialized;

    this.initCustomColorInput();
    this.initTextureInput();
    this.initSchemeButtons();
    this.initColorPages();

    const darkModeChange = this.handleDarkModeChange.bind(this);
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', darkModeChange);

    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      'windowSchemeType',
      'zen.view.window.scheme',
      2,
      darkModeChange
    );

    XPCOMUtils.defineLazyPreferenceGetter(this, 'darkModeBias', 'zen.theme.dark-mode-bias', '0.5');
  }

  handleDarkModeChange() {
    this.updateCurrentWorkspace();
  }

  get isDarkMode() {
    if (PrivateBrowsingUtils.isWindowPrivate(window)) {
      return true;
    }
    switch (this.windowSchemeType) {
      case 0:
        return true;
      case 1:
        return false;
      default:
    }
    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  }

  get colorHarmonies() {
    return [
      { type: 'complementary', angles: [180] },
      { type: 'singleAnalogous', angles: [310] },
      { type: 'splitComplementary', angles: [150, 210] },
      { type: 'analogous', angles: [50, 310] },
      { type: 'triadic', angles: [120, 240] },
      { type: 'floating', angles: [] },
    ];
  }

  initContextMenu() {
    const menu = window.MozXULElement.parseXULToFragment(`
        <menuitem id="zenToolbarThemePicker"
                  data-lazy-l10n-id="zen-workspaces-change-theme"
                  command="cmd_zenOpenZenThemePicker"/>
      `);
    document.getElementById('toolbar-context-customize').before(menu);
  }

  openThemePicker(event) {
    const fromForm = event.explicitOriginalTarget?.classList?.contains(
      'zen-workspace-creation-edit-theme-button'
    );
    PanelMultiView.openPopup(this.panel, this.toolbox, {
      position: 'topright topleft',
      triggerEvent: event,
      y: fromForm ? -160 : 0,
    });
  }

  initCustomColorInput() {
    this.customColorInput.addEventListener('change', (event) => {
      // Prevent the popup from closing when the input is focused
      this.openThemePicker(event);
    });
  }

  initPredefinedColors() {
    document
      .getElementById('PanelUI-zen-gradient-generator-color-pages')
      .addEventListener('click', async (event) => {
        const target = event.target;
        const rawPosition = target.getAttribute('data-position');
        if (!rawPosition) {
          return;
        }
        const algo = target.getAttribute('data-algo');
        const lightness = target.getAttribute('data-lightness');
        const numDots = parseInt(target.getAttribute('data-num-dots'));
        if (numDots < this.dots.length) {
          for (let i = numDots; i < this.dots.length; i++) {
            this.dots[i].element.remove();
          }
          this.dots = this.dots.slice(0, numDots);
        }
        const type = target.getAttribute('data-type') || EXPLICIT_LIGHTNESS_TYPE;
        // Generate new gradient from the single color given
        const [x, y] = rawPosition.split(',').map((pos) => parseInt(pos));
        let dots = [
          {
            ID: 0,
            position: { x, y },
            isPrimary: true,
            type,
          },
        ];
        for (let i = 1; i < numDots; i++) {
          dots.push({
            ID: i,
            position: { x: 0, y: 0 },
            type,
          });
        }
        this.useAlgo = algo;
        if (lightness !== null) this.#currentLightness = lightness;
        dots = this.calculateCompliments(dots, 'update', this.useAlgo);
        this.handleColorPositions(dots, true);
        this.updateCurrentWorkspace();
      });
  }

  initColorPages() {
    const leftButton = document.getElementById('PanelUI-zen-gradient-generator-color-page-left');
    const rightButton = document.getElementById('PanelUI-zen-gradient-generator-color-page-right');
    const pagesWrapper = document.getElementById('PanelUI-zen-gradient-generator-color-pages');
    const pages = pagesWrapper.children;
    pagesWrapper.addEventListener('wheel', (event) => {
      event.preventDefault();
      event.stopPropagation();
    });
    leftButton.addEventListener('command', () => {
      this.#colorPage = (this.#colorPage - 1 + pages.length) % pages.length;
      // Scroll to the next page, by using scrollLeft
      pagesWrapper.scrollLeft = (this.#colorPage * pagesWrapper.scrollWidth) / pages.length;
      rightButton.disabled = false;
      leftButton.disabled = this.#colorPage === 0;
    });
    rightButton.addEventListener('command', () => {
      this.#colorPage = (this.#colorPage + 1) % pages.length;
      // Scroll to the next page, by using scrollLeft
      pagesWrapper.scrollLeft = (this.#colorPage * pagesWrapper.scrollWidth) / pages.length;
      leftButton.disabled = false;
      rightButton.disabled = this.#colorPage === pages.length - 1;
    });
  }

  initSchemeButtons() {
    const buttons = document.getElementById('PanelUI-zen-gradient-generator-scheme');
    buttons.addEventListener('click', (event) => {
      const target = event.target.closest('.subviewbutton');
      if (!target) {
        return;
      }
      event.preventDefault();
      event.stopPropagation();
      const scheme = target.id.replace('PanelUI-zen-gradient-generator-scheme-', '');
      if (!scheme) {
        return;
      }
      const themeInt = {
        auto: 2,
        light: 1,
        dark: 0,
      }[scheme];
      if (themeInt === undefined) {
        return;
      }
      Services.prefs.setIntPref('zen.view.window.scheme', themeInt);
    });
  }

  initTextureInput() {
    const wrapper = document.getElementById('PanelUI-zen-gradient-generator-texture-wrapper');
    const wrapperWidth = wrapper.getBoundingClientRect().width;
    // Add elements in a circular pattern, where the center is the center of the wrapper
    for (let i = 0; i < 16; i++) {
      const dot = document.createElement('div');
      dot.classList.add('zen-theme-picker-texture-dot');
      const position = (i / 16) * Math.PI * 2 + wrapperWidth;
      dot.style.left = `${Math.cos(position) * 50 + 50}%`;
      dot.style.top = `${Math.sin(position) * 50 + 50}%`;
      wrapper.appendChild(dot);
    }
    this._textureHandler = document.createElement('div');
    this._textureHandler.id = 'PanelUI-zen-gradient-generator-texture-handler';
    this._textureHandler.addEventListener('mousedown', this.onTextureHandlerMouseDown.bind(this));
    wrapper.appendChild(this._textureHandler);
  }

  onTextureHandlerMouseDown(event) {
    event.preventDefault();
    this._onTextureMouseMove = this.onTextureMouseMove.bind(this);
    this._onTextureMouseUp = this.onTextureMouseUp.bind(this);
    document.addEventListener('mousemove', this._onTextureMouseMove);
    document.addEventListener('mouseup', this._onTextureMouseUp);
  }

  onTextureMouseMove(event) {
    event.preventDefault();
    const wrapper = document.getElementById('PanelUI-zen-gradient-generator-texture-wrapper');
    const wrapperRect = wrapper.getBoundingClientRect();
    // Determine how much rotation there is based on the mouse position and the center of the wrapper
    const rotation = Math.atan2(
      event.clientY - wrapperRect.top - wrapperRect.height / 2,
      event.clientX - wrapperRect.left - wrapperRect.width / 2
    );
    const previousTexture = this.currentTexture;
    this.currentTexture = (rotation * 180) / Math.PI + 90;
    // if it's negative, add 360 to make it positive
    if (this.currentTexture < 0) {
      this.currentTexture += 360;
    }
    // make it go from 1 to 0 instead of being in degrees
    this.currentTexture /= 360;
    // We clip it to the closest button out of 16 possible buttons
    this.currentTexture = Math.round(this.currentTexture * 16) / 16;
    if (this.currentTexture === 1) {
      this.currentTexture = 0;
    }
    if (previousTexture !== this.currentTexture) {
      this.updateCurrentWorkspace();
      Services.zen.playHapticFeedback();
    }
  }

  onTextureMouseUp(event) {
    event.preventDefault();
    document.removeEventListener('mousemove', this._onTextureMouseMove);
    document.removeEventListener('mouseup', this._onTextureMouseUp);
    this._onTextureMouseMove = null;
    this._onTextureMouseUp = null;
  }

  initThemePicker() {
    const themePicker = this.panel.querySelector('.zen-theme-picker-gradient');
    this._onDotMouseMove = this.onDotMouseMove.bind(this);
    this._onDotMouseUp = this.onDotMouseUp.bind(this);
    this._onDotMouseDown = this.onDotMouseDown.bind(this);
    this._onThemePickerClick = this.onThemePickerClick.bind(this);
    document.addEventListener('mousemove', this._onDotMouseMove);
    document.addEventListener('mouseup', this._onDotMouseUp);
    themePicker.addEventListener('mousedown', this._onDotMouseDown);
    themePicker.addEventListener('click', this._onThemePickerClick);
  }

  uninitThemePicker() {
    const themePicker = this.panel.querySelector('.zen-theme-picker-gradient');
    document.removeEventListener('mousemove', this._onDotMouseMove);
    document.removeEventListener('mouseup', this._onDotMouseUp);
    themePicker.removeEventListener('mousedown', this._onDotMouseDown);
    themePicker.removeEventListener('click', this._onThemePickerClick);
    this._onDotMouseMove = null;
    this._onDotMouseUp = null;
    this._onDotMouseDown = null;
    this._onThemePickerClick = null;
  }

  /**
   * Converts an HSL color value to RGB. Conversion formula
   * adapted from https://en.wikipedia.org/wiki/HSL_color_space.
   * Assumes h, s, and l are contained in the set [0, 1] and
   * returns r, g, and b in the set [0, 255].
   *
   * @param   {number}  h       The hue
   * @param   {number}  s       The saturation
   * @param   {number}  l       The lightness
   * @return  {Array}           The RGB representation
   */
  hslToRgb(h, s, l) {
    const { round } = Math;
    let r, g, b;

    if (s === 0) {
      r = g = b = l; // achromatic
    } else {
      const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
      const p = 2 * l - q;
      r = this.hueToRgb(p, q, h + 1 / 3);
      g = this.hueToRgb(p, q, h);
      b = this.hueToRgb(p, q, h - 1 / 3);
    }

    return [round(r * 255), round(g * 255), round(b * 255)];
  }

  rgbToHsl(r, g, b) {
    r /= 255;
    g /= 255;
    b /= 255;
    let max = Math.max(r, g, b);
    let min = Math.min(r, g, b);
    let d = max - min;
    let h;
    if (d === 0) h = 0;
    else if (max === r) h = ((g - b) / d) % 6;
    else if (max === g) h = (b - r) / d + 2;
    else if (max === b) h = (r - g) / d + 4;
    let l = (min + max) / 2;
    let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
    return [h * 60, s, l];
  }

  hueToRgb(p, q, t) {
    if (t < 0) t += 1;
    if (t > 1) t -= 1;
    if (t < 1 / 6) return p + (q - p) * 6 * t;
    if (t < 1 / 2) return q;
    if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
    return p;
  }

  calculateInitialPosition([r, g, b]) {
    // This function is called before the picker is even rendered, so we hard code the dimensions
    // important: If any sort of sizing is changed, make sure changes are reflected here
    const padding = 0;
    const rect = {
      width: 380 + padding * 2,
      height: 380 + padding * 2,
    };
    const centerX = rect.width / 2;
    const centerY = rect.height / 2;
    const radius = (rect.width - padding) / 2;
    const [hue, saturation] = this.rgbToHsl(r, g, b);
    const angle = (hue / 360) * 2 * Math.PI; // Convert to radians
    const normalizedSaturation = saturation / 100; // Convert to [0, 1]
    const x = centerX + radius * normalizedSaturation * Math.cos(angle) - padding;
    const y = centerY + radius * normalizedSaturation * Math.sin(angle) - padding;
    return { x, y };
  }

  getColorFromPosition(x, y, type = undefined) {
    // Return a color as hsl based on the position in the gradient
    const gradient = this.panel.querySelector('.zen-theme-picker-gradient');
    const rect = gradient.getBoundingClientRect();
    const padding = 30; // each side
    const dotHalfSize = 38 / 2; // half the size of the dot
    x += dotHalfSize;
    y += dotHalfSize;
    rect.width += padding * 2; // Adjust width and height for padding
    rect.height += padding * 2; // Adjust width and height for padding
    const centerX = rect.width / 2;
    const centerY = rect.height / 2;
    const radius = (rect.width - padding) / 2;
    const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
    let angle = Math.atan2(y - centerY, x - centerX);
    angle = (angle * 180) / Math.PI; // Convert to degrees
    if (angle < 0) {
      angle += 360; // Normalize to [0, 360)
    }
    const normalizedDistance = 1 - Math.min(distance / radius, 1); // Normalize distance to [0, 1]
    let hue = (angle / 360) * 360; // Normalize angle to [0, 360)
    let saturation = normalizedDistance * 100; // stays high even in center
    if (type !== EXPLICIT_LIGHTNESS_TYPE) {
      saturation = 80 + (1 - normalizedDistance) * 20;
      // Set the current lightness to how far we are from the center of the circle
      // For example, moving the dot outside will have higher lightness, while moving it inside will have lower lightness
      this.#currentLightness = Math.round((1 - normalizedDistance) * 100);
    }
    let lightness = this.#currentLightness; // Fixed lightness for simplicity
    if (type === EXPLICIT_BLACKWHITE_TYPE) {
      // We can only get grayscales from white to black
      saturation = 0;
      lightness = Math.round((1 - normalizedDistance) * 100);
    }
    const [r, g, b] = this.hslToRgb(hue / 360, saturation / 100, lightness / 100);
    return [
      Math.min(255, Math.max(0, r)),
      Math.min(255, Math.max(0, g)),
      Math.min(255, Math.max(0, b)),
    ];
  }

  getJSONPos(x, y) {
    // Return a JSON string with the position
    return JSON.stringify({ x: Math.round(x), y: Math.round(y) });
  }

  createDot(color, fromWorkspace = false) {
    const [r, g, b] = color.c;
    const dot = document.createElement('div');
    if (color.isPrimary) {
      dot.classList.add('primary');
    }
    if (color.isCustom) {
      if (!color.c) {
        return;
      }
      dot.classList.add('custom');
      dot.style.opacity = 0;
      dot.style.setProperty('--zen-theme-picker-dot-color', color.c);
    } else {
      const { x, y } = color.position || this.calculateInitialPosition([r, g, b]);
      const dotPad = this.panel.querySelector('.zen-theme-picker-gradient');

      dot.classList.add('zen-theme-picker-dot');

      dot.style.left = `${x}px`;
      dot.style.top = `${y}px`;

      if (this.dots.length < 1) {
        dot.classList.add('primary');
      }

      dotPad.appendChild(dot);
      let id = this.dots.length;

      dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${r}, ${g}, ${b})`);
      dot.setAttribute('data-position', this.getJSONPos(x, y));
      dot.setAttribute('data-type', color.type);

      this.dots.push({
        ID: id,
        element: dot,
        position: { x, y },
        type: color.type,
        lightness: color.lightness,
      });
    }
    if (!fromWorkspace) {
      this.updateCurrentWorkspace(true);
    }
  }

  addColorToCustomList(color) {
    const listItems = window.MozXULElement.parseXULToFragment(`
        <hbox class="zen-theme-picker-custom-list-item">
          <html:div class="zen-theme-picker-dot custom"></html:div>
          <label class="zen-theme-picker-custom-list-item-label"></label>
          <toolbarbutton class="zen-theme-picker-custom-list-item-remove toolbarbutton-1"></toolbarbutton>
        </hbox>
      `);
    listItems.querySelector('.zen-theme-picker-custom-list-item').setAttribute('data-color', color);
    listItems
      .querySelector('.zen-theme-picker-dot')
      .style.setProperty('--zen-theme-picker-dot-color', color);
    listItems.querySelector('.zen-theme-picker-custom-list-item-label').textContent = color;
    listItems
      .querySelector('.zen-theme-picker-custom-list-item-remove')
      .addEventListener('command', this.removeCustomColor.bind(this));

    this.customColorList.appendChild(listItems);
  }

  async addCustomColor() {
    let color = this.customColorInput.value;

    if (!color) {
      return;
    }

    let colorOpacity =
      document.getElementById('PanelUI-zen-gradient-generator-custom-opacity')?.value ?? 1;
    // Convert the opacity into a hex value if it's not already
    if (colorOpacity < 1) {
      // e.g. if opacity is 1, we add to the color FF, if it's 0.5 we add 80, etc.
      const hexOpacity = Math.round(colorOpacity * 255)
        .toString(16)
        .padStart(2, '0')
        .toUpperCase();
      // If the color is in hex format
      if (color.startsWith('#')) {
        // If the color is already in hex format, we just append the opacity
        if (color.length === 7) {
          color += hexOpacity;
        }
      }
    }

    // Add '#' prefix if it's missing and the input appears to be a hex color
    if (!color.startsWith('#') && /^[0-9A-Fa-f]{3,6}$/.test(color)) {
      color = '#' + color;
    }

    // can be any color format, we just add it to the list as a dot, but hidden
    const dot = document.createElement('div');
    dot.classList.add('zen-theme-picker-dot', 'hidden', 'custom');
    dot.style.opacity = 0;
    dot.style.setProperty('--zen-theme-picker-dot-color', color);
    this.panel.querySelector('#PanelUI-zen-gradient-generator-custom-list').prepend(dot);
    this.customColorInput.value = '';
    document.getElementById('PanelUI-zen-gradient-generator-custom-opacity').value = 1;
    await this.updateCurrentWorkspace();
  }

  handlePanelCommand(event) {
    const target = event.target.closest('toolbarbutton');
    if (!target) {
      return;
    }
    switch (target.id) {
      case 'PanelUI-zen-gradient-generator-color-custom-add':
        this.addCustomColor();
        break;
    }
  }

  spawnDot(dotData, primary = false) {
    const dotPad = this.panel.querySelector('.zen-theme-picker-gradient');
    const relativePosition = {
      x: dotData.x,
      y: dotData.y,
    };

    const dot = document.createElement('div');
    dot.classList.add('zen-theme-picker-dot');

    dot.style.left = `${dotData.x}px`;
    dot.style.top = `${dotData.y}px`;

    dotPad.appendChild(dot);

    let id = this.dots.length;

    if (primary) {
      id = 0;
      dot.classList.add('primary');

      const existingPrimaryDot = this.dots.find((d) => d.ID === 0);
      if (existingPrimaryDot) {
        existingPrimaryDot.ID = this.dots.length;
        existingPrimaryDot.element.classList.remove('primary');
      }
    }

    const colorFromPos = this.getColorFromPosition(
      relativePosition.x,
      relativePosition.y,
      dotData.type
    );
    dot.style.setProperty(
      '--zen-theme-picker-dot-color',
      `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})`
    );
    dot.setAttribute('data-position', this.getJSONPos(relativePosition.x, relativePosition.y));
    dot.setAttribute('data-type', dotData.type);

    this.dots.push({
      ID: id,
      element: dot,
      position: { x: relativePosition.x, y: relativePosition.y },
      lightness: this.#currentLightness,
      type: dotData.type,
    });
  }

  calculateCompliments(dots, action = 'update', useHarmony = '') {
    const colorHarmonies = this.colorHarmonies;

    if (dots.length === 0) {
      return [];
    }

    function getColorHarmonyType(numDots, dots) {
      if (useHarmony !== '') {
        const selectedHarmony = colorHarmonies.find((harmony) => harmony.type === useHarmony);

        if (selectedHarmony) {
          if (action === 'remove') {
            if (dots.length !== 0) {
              return colorHarmonies.find(
                (harmony) => harmony.angles.length === selectedHarmony.angles.length - 1
              );
            } else {
              return { type: 'floating', angles: [] };
            }
          }
          if (action === 'add') {
            return colorHarmonies.find(
              (harmony) => harmony.angles.length === selectedHarmony.angles.length + 1
            );
          }
          if (action === 'update') {
            return selectedHarmony;
          }
        }
      }

      if (action === 'remove') {
        let harmony = colorHarmonies.find((harmony) => harmony.angles.length === numDots);
        // If we are coming from 3 analogous dots, we should now go to singleAnalogous if
        // there are 2 dots left
        if (harmony.type === 'analogous' && numDots === 1) {
          harmony = colorHarmonies.find((harmony) => harmony.type === 'singleAnalogous');
        }
        return harmony;
      }
      if (action === 'add') {
        return colorHarmonies.find((harmony) => harmony.angles.length + 1 === numDots);
      }
      if (action === 'update') {
        return colorHarmonies.find((harmony) => harmony.angles.length + 1 === numDots);
      }
    }

    function getAngleFromPosition(position, centerPosition) {
      let deltaX = position.x - centerPosition.x;
      let deltaY = position.y - centerPosition.y;
      let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
      return (angle + 360) % 360;
    }

    function getDistanceFromCenter(position, centerPosition) {
      const deltaX = position.x - centerPosition.x;
      const deltaY = position.y - centerPosition.y;
      return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
    }

    const dotPad = this.panel.querySelector('.zen-theme-picker-gradient');
    const rect = dotPad.getBoundingClientRect();
    const padding = 0;

    let updatedDots = [...dots];
    const centerPosition = { x: rect.width / 2, y: rect.height / 2 };

    const harmonyAngles = getColorHarmonyType(
      dots.length + (action === 'add' ? 1 : action === 'remove' ? -1 : 0),
      this.dots
    );
    this.useAlgo = harmonyAngles.type;
    if (!harmonyAngles || harmonyAngles.angles.length === 0) return dots;

    let primaryDot = dots.find((dot) => dot.ID === 0);
    if (!primaryDot) return [];

    if (action === 'add' && this.dots.length) {
      updatedDots.push({ ID: this.dots.length, position: centerPosition });
    }
    const baseAngle = getAngleFromPosition(primaryDot.position, centerPosition);
    let distance = getDistanceFromCenter(primaryDot.position, centerPosition);
    const radius = (rect.width - padding) / 2;
    if (distance > radius) distance = radius;
    if (this.dots.length > 0) {
      updatedDots = [
        {
          ID: 0,
          position: primaryDot.position,
          type: primaryDot.type,
        },
      ];
    }

    harmonyAngles.angles.forEach((angleOffset, index) => {
      let newAngle = (baseAngle + angleOffset) % 360;
      let radian = (newAngle * Math.PI) / 180;

      let newPosition = {
        x: centerPosition.x + distance * Math.cos(radian),
        y: centerPosition.y + distance * Math.sin(radian),
      };

      updatedDots.push({
        ID: index + 1,
        position: newPosition,
        type: primaryDot.type,
      });
    });

    return updatedDots;
  }

  handleColorPositions(colorPositions, ignoreLegacy = false) {
    colorPositions.sort((a, b) => a.ID - b.ID);

    if (this.isLegacyVersion && !ignoreLegacy) {
      this.isLegacyVersion = false;
      Services.prefs.setIntPref('zen.theme.gradient-legacy-version', 1);
    }

    colorPositions.forEach((dotPosition) => {
      const existingDot = this.dots.find((dot) => dot.ID === dotPosition.ID);

      if (existingDot) {
        existingDot.type = dotPosition.type;
        existingDot.position = dotPosition.position;
        const colorFromPos = this.getColorFromPosition(
          dotPosition.position.x,
          dotPosition.position.y,
          dotPosition.type
        );
        existingDot.lightness = this.#currentLightness;
        existingDot.element.style.setProperty(
          '--zen-theme-picker-dot-color',
          `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})`
        );
        existingDot.element.setAttribute(
          'data-position',
          this.getJSONPos(dotPosition.position.x, dotPosition.position.y)
        );
        existingDot.element.setAttribute('data-type', dotPosition.type);

        if (!this.dragging) {
          gZenUIManager.motion.animate(
            existingDot.element,
            {
              left: existingDot.element.style.left
                ? [existingDot.element.style.left, `${dotPosition.position.x}px`]
                : `${dotPosition.position.x}px`,
              top: existingDot.element.style.top
                ? [existingDot.element.style.top, `${dotPosition.position.y}px`]
                : `${dotPosition.position.y}px`,
            },
            {
              duration: 0.4,
              type: 'spring',
              bounce: 0.3,
            }
          );
        } else {
          existingDot.element.style.left = `${dotPosition.position.x}px`;
          existingDot.element.style.top = `${dotPosition.position.y}px`;
        }
      } else {
        this.spawnDot({
          type: dotPosition.type,
          ...dotPosition.position,
        });
      }
    });
  }

  onThemePickerClick(event) {
    if (this._rotating) {
      return;
    }
    if (event.target.closest('#PanelUI-zen-gradient-generator-scheme')) {
      return;
    }
    event.preventDefault();
    const target = event.target;
    if (target.id === 'PanelUI-zen-gradient-generator-color-add') {
      if (this.dots.length >= nsZenThemePicker.MAX_DOTS) return;
      let colorPositions = this.calculateCompliments(this.dots, 'add', this.useAlgo);

      this.handleColorPositions(colorPositions);
      this.updateCurrentWorkspace();
      return;
    } else if (target.id === 'PanelUI-zen-gradient-generator-color-remove') {
      this.dots.sort((a, b) => a.ID - b.ID);
      if (this.dots.length === 0) return;

      const lastDot = this.dots.pop();
      lastDot.element.remove();

      this.dots.forEach((dot, index) => {
        dot.ID = index;
        if (index === 0) {
          dot.element.classList.add('primary');
        } else {
          dot.element.classList.remove('primary');
        }
      });

      let colorPositions = this.calculateCompliments(this.dots, 'remove');
      this.handleColorPositions(colorPositions);
      this.updateCurrentWorkspace();
      return;
    } else if (target.id === 'PanelUI-zen-gradient-generator-color-toggle-algo') {
      const applicableHarmonies = this.colorHarmonies.filter(
        (harmony) => harmony.angles.length + 1 === this.dots.length
      );

      let currentIndex = applicableHarmonies.findIndex((harmony) => harmony.type === this.useAlgo);

      const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % applicableHarmonies.length;
      this.useAlgo = applicableHarmonies[nextIndex].type;

      let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
      this.handleColorPositions(colorPositions);
      this.updateCurrentWorkspace();
      return;
    }

    if (event.button !== 0 || this.dragging || this.recentlyDragged) return;

    const gradient = this.panel.querySelector('.zen-theme-picker-gradient');
    const rect = gradient.getBoundingClientRect();
    const padding = 0;

    const centerX = rect.left + rect.width / 2 - padding;
    const centerY = rect.top + rect.height / 2 - padding;
    const radius = (rect.width - padding) / 2;
    let pixelX = event.clientX;
    let pixelY = event.clientY;

    const clickedElement = event.target;
    let clickedDot = null;
    const existingPrimaryDot = this.dots.find((d) => d.ID === 0);

    clickedDot = this.dots.find((dot) => dot.element === clickedElement);

    if (clickedDot) {
      // TODO: this doesnt work and needs to be fixed
      existingPrimaryDot.ID = clickedDot.ID;
      clickedDot.ID = 0;
      clickedDot.element.style.zIndex = 999;

      let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
      this.handleColorPositions(colorPositions);
      return;
    }

    const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2);
    if (distance > radius) {
      const angle = Math.atan2(pixelY - centerY, pixelX - centerX);
      pixelX = centerX + Math.cos(angle) * radius;
      pixelY = centerY + Math.sin(angle) * radius;
    }

    const relativeX = pixelX - rect.left;
    const relativeY = pixelY - rect.top;

    if (!clickedDot && this.dots.length < 1) {
      this.spawnDot({ x: relativeX, y: relativeY }, this.dots.length === 0);

      this.updateCurrentWorkspace(true);
    } else if (!clickedDot && existingPrimaryDot) {
      existingPrimaryDot.position = {
        x: relativeX,
        y: relativeY,
      };

      let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
      this.handleColorPositions(colorPositions);
      this.updateCurrentWorkspace(true);

      gZenUIManager.motion.animate(
        existingPrimaryDot.element,
        {
          left: `${existingPrimaryDot.position.x}px`,
          top: `${existingPrimaryDot.position.y}px`,
        },
        {
          duration: 0.4,
          type: 'spring',
          bounce: 0.3,
        }
      );
    }
  }

  onDotMouseDown(event) {
    if (event.button === 2) {
      return;
    }
    const draggedDot = this.dots.find((dot) => dot.element === event.target);
    if (draggedDot) {
      event.preventDefault();
      this.dragging = true;
      this.draggedDot = event.target;
      this.draggedDot.classList.add('dragging');
    }

    // Store the starting position of the drag
    this.dragStartPosition = {
      x: event.clientX,
      y: event.clientY,
    };
  }

  onDotMouseUp(event) {
    if (this._rotating) {
      return;
    }
    if (event.button === 2) {
      if (!event.target.classList.contains('zen-theme-picker-dot')) {
        return;
      }
      this.dots = this.dots.filter((dot) => dot.element !== event.target);
      event.target.remove();

      this.dots.sort((a, b) => a.ID - b.ID);

      // Reassign the IDs after sorting
      this.dots.forEach((dot, index) => {
        dot.ID = index;
        if (index === 0) {
          dot.element.classList.add('primary');
        } else {
          dot.element.classList.remove('primary');
        }
      });

      let colorPositions = this.calculateCompliments(this.dots, 'remove');
      this.handleColorPositions(colorPositions);

      this.updateCurrentWorkspace();
      return;
    }

    if (this.dragging) {
      event.preventDefault();
      event.stopPropagation();
      this.dragging = false;
      this.draggedDot.classList.remove('dragging');
      this.draggedDot = null;
      this.dragStartPosition = null; // Reset the drag start position

      this.recentlyDragged = true;
      setTimeout(() => {
        this.recentlyDragged = false;
      }, 100);
      return;
    }
  }

  onDotMouseMove(event) {
    if (this.dragging) {
      event.preventDefault();
      const rect = this.panel.querySelector('.zen-theme-picker-gradient').getBoundingClientRect();
      const padding = 0; // each side
      // do NOT let the ball be draged outside of an imaginary circle. You can drag it anywhere inside the circle
      // if the distance between the center of the circle and the dragged ball is bigger than the radius, then the ball
      // should be placed on the edge of the circle. If it's inside the circle, then the ball just follows the mouse

      const centerX = rect.left + rect.width / 2;
      const centerY = rect.top + rect.height / 2;
      const radius = (rect.width - padding) / 2;
      let pixelX = event.clientX;
      let pixelY = event.clientY;
      const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2);
      if (distance > radius) {
        const angle = Math.atan2(pixelY - centerY, pixelX - centerX);
        pixelX = centerX + Math.cos(angle) * radius;
        pixelY = centerY + Math.sin(angle) * radius;
      }

      // set the location of the dot in pixels
      const relativeX = pixelX - rect.left;
      const relativeY = pixelY - rect.top;

      const draggedDot = this.dots.find((dot) => dot.element === this.draggedDot);
      draggedDot.element.style.left = `${relativeX}px`;
      draggedDot.element.style.top = `${relativeY}px`;
      draggedDot.position = {
        x: relativeX,
        y: relativeY,
      };
      let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
      this.handleColorPositions(colorPositions);

      this.updateCurrentWorkspace();
    }
  }

  themedColors(colors) {
    // For non-Mica themes, we return the colors as they are
    return [...colors];
  }

  onOpacityChange(event) {
    this.currentOpacity = parseFloat(event.target.value);
    // If we reached a whole number (e.g., 0.1, 0.2, etc.), send a haptic feedback.
    if (Math.round(this.currentOpacity * 10) !== this._lastHapticFeedback) {
      Services.zen.playHapticFeedback();
      this._lastHapticFeedback = Math.round(this.currentOpacity * 10);
    }
    this.updateCurrentWorkspace();
  }

  getToolbarModifiedBaseRaw() {
    const opacity = this.#allowTransparencyOnSidebar ? 0.6 : 1;
    return this.isDarkMode ? [23, 23, 26, opacity] : [240, 240, 244, opacity];
  }

  getToolbarModifiedBase() {
    const baseColor = this.getToolbarModifiedBaseRaw();
    return `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${baseColor[3]})`;
  }

  get isMica() {
    return window.matchMedia('(-moz-windows-mica)').matches;
  }

  get canBeTransparent() {
    return (
      this.isMica ||
      window.matchMedia(
        '(-moz-platform: macos) or ((-moz-platform: linux) and -moz-pref("zen.widget.linux.transparency"))'
      ).matches
    );
  }

  blendWithWhiteOverlay(baseColor, opacity) {
    let colorToBlend;
    let colorToBlendOpacity;
    if (this.isMica) {
      colorToBlend = !this.isDarkMode ? [0, 0, 0] : [255, 255, 255];
      colorToBlendOpacity = 0.35;
    } else if (AppConstants.platform === 'macosx') {
      colorToBlend = [255, 255, 255];
      colorToBlendOpacity = 0.3;
    }
    if (colorToBlend) {
      const blendedAlpha = Math.min(
        1,
        opacity + MIN_OPACITY + colorToBlendOpacity * (1 - (opacity + MIN_OPACITY))
      );
      baseColor = this.blendColors(baseColor, colorToBlend, blendedAlpha * 100);
      opacity += colorToBlendOpacity * (1 - opacity);
    }
    return `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${opacity})`;
  }

  getSingleRGBColor(color, forToolbar = false) {
    if (color.isCustom) {
      return color.c;
    }
    let opacity = this.currentOpacity;
    if (
      (forToolbar && !this.#allowTransparencyOnSidebar) ||
      (!forToolbar && !this.canBeTransparent)
    ) {
      color = this.blendColors(
        color.c,
        this.getToolbarModifiedBaseRaw().slice(0, 3),
        this.canBeTransparent ? 90 : opacity * 100
      );
      opacity = 1; // Toolbar colors should always be fully opaque
    } else {
      color = color.c;
    }
    if (this.isLegacyVersion && this.isDarkMode) {
      // In legacy version, we blend with white overlay or black overlay based on if we are in dark mode
      color = this.blendColors(color, [0, 0, 0], 30);
    }
    return this.blendWithWhiteOverlay(color, opacity);
  }

  luminance([r, g, b]) {
    // These magic numbers are extracted from the wikipedia article on relative luminance
    // https://en.wikipedia.org/wiki/Relative_luminance
    var a = [r, g, b].map((v) => {
      v /= 255;
      return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
    });
    return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
  }

  contrastRatio(rgb1, rgb2) {
    const lum1 = this.luminance(rgb1);
    const lum2 = this.luminance(rgb2);
    const brightest = Math.max(lum1, lum2);
    const darkest = Math.min(lum1, lum2);
    return (brightest + 0.05) / (darkest + 0.05);
  }

  blendColors(rgb1, rgb2, percentage) {
    const p = percentage / 100;
    return [
      Math.round(rgb1[0] * p + rgb2[0] * (1 - p)),
      Math.round(rgb1[1] * p + rgb2[1] * (1 - p)),
      Math.round(rgb1[2] * p + rgb2[2] * (1 - p)),
    ];
  }

  getGradient(colors, forToolbar = false) {
    const themedColors = this.themedColors(colors);
    this.useAlgo = themedColors[0]?.algorithm ?? '';
    this.#currentLightness = themedColors[0]?.lightness ?? 50;

    const rotation = -45; // TODO: Detect rotation based on the accent color
    if (themedColors.length === 0) {
      const getBrowserBg = () => {
        if (this.canBeTransparent) {
          return this.isDarkMode ? 'rgba(0, 0, 0, 0.4)' : 'transparent';
        }
        return this.isDarkMode ? '#131313' : '#e9e9e9';
      };
      return forToolbar ? this.getToolbarModifiedBase() : getBrowserBg();
    } else if (themedColors.length === 1) {
      return this.getSingleRGBColor(themedColors[0], forToolbar);
    } else {
      // If there are custom colors, we just return a linear gradient with all colors
      if (themedColors.find((color) => color.isCustom)) {
        // Just return a linear gradient with all colors
        const gradientColors = themedColors.map((color) =>
          this.getSingleRGBColor(color, forToolbar)
        );
        // Divide all colors evenly in the gradient
        const colorStops = gradientColors
          .map((color, index) => {
            const position = (index / (gradientColors.length - 1)) * 100;
            return `${color} ${position}%`;
          })
          .join(', ');
        return `linear-gradient(${rotation}deg, ${colorStops})`;
      }
      if (themedColors.length === 2) {
        if (!forToolbar) {
          return [
            `linear-gradient(${rotation}deg, ${this.getSingleRGBColor(themedColors[1], forToolbar)} 0%, transparent 100%)`,
            `linear-gradient(${rotation + 180}deg, ${this.getSingleRGBColor(themedColors[0], forToolbar)} 0%, transparent 100%)`,
          ]
            .reverse()
            .join(', ');
        }
        return `linear-gradient(${rotation}deg, ${this.getSingleRGBColor(themedColors[1], forToolbar)} 0%, ${this.getSingleRGBColor(themedColors[0], forToolbar)} 100%)`;
      } else if (themedColors.length === 3) {
        let color1 = this.getSingleRGBColor(themedColors[2], forToolbar);
        let color2 = this.getSingleRGBColor(themedColors[0], forToolbar);
        let color3 = this.getSingleRGBColor(themedColors[1], forToolbar);
        return [
          `radial-gradient(circle at 0% 0%, ${color2} -10%, transparent 100%)`,
          `linear-gradient(to top, ${color1} -50%, transparent 125%)`,
          `radial-gradient(circle at 100% -100%, ${color3} -100%, transparent 400%)`,
        ].join(', ');
      }
    }
  }

  shouldBeDarkMode(accentColor) {
    if (Services.prefs.getBoolPref('zen.theme.use-system-colors')) {
      return this.isDarkMode;
    }

    if (!this.canBeTransparent) {
      const toolbarBg = this.getToolbarModifiedBaseRaw();
      accentColor = this.blendColors(
        toolbarBg.slice(0, 3),
        accentColor,
        (1 - this.currentOpacity) * 100
      );
    }

    const bg = accentColor;

    // Get text colors (with alpha)
    let darkText = this.getToolbarColor(true); // e.g. [r, g, b, a]
    let lightText = this.getToolbarColor(false); // e.g. [r, g, b, a]

    if (this.canBeTransparent) {
      lightText[3] -= parseFloat(this.darkModeBias); // Reduce alpha for light text
    }

    // Composite text color over background
    darkText = this.blendColors(bg, darkText.slice(0, 3), (1 - darkText[3]) * 100);
    lightText = this.blendColors(bg, lightText.slice(0, 3), (1 - lightText[3]) * 100);

    const darkContrast = this.contrastRatio(bg, darkText);
    const lightContrast = this.contrastRatio(bg, lightText);

    return darkContrast > lightContrast;
  }

  static getTheme(colors = [], opacity = 0.5, texture = 0) {
    return {
      type: 'gradient',
      gradientColors: colors ? colors.filter((color) => color) : [], // remove undefined
      opacity,
      texture,
    };
  }

  updateNoise(texture) {
    document.documentElement.style.setProperty('--zen-grainy-background-opacity', texture);
    document.documentElement.setAttribute(
      'zen-show-grainy-background',
      texture > 0 ? 'true' : 'false'
    );
  }

  hexToRgb(hex) {
    if (hex.startsWith('#')) {
      hex = hex.substring(1);
    }
    if (hex.length === 3) {
      hex = hex
        .split('')
        .map((char) => char + char)
        .join('');
    }
    return [
      parseInt(hex.substring(0, 2), 16),
      parseInt(hex.substring(2, 4), 16),
      parseInt(hex.substring(4, 6), 16),
    ];
  }

  /**
   * Get the primary color from a list of colors.
   * @returns {string} The primary color in hex format.
   */
  getAccentColorForUI(accentColor) {
    return `rgb(${accentColor[0]}, ${accentColor[1]}, ${accentColor[2]})`;
  }

  getMostDominantColor(allColors) {
    const color = this.getPrimaryColor(allColors);
    if (typeof color === 'string') {
      // We found a custom color, we should rather return the native accent color
      return this.getNativeAccentColor();
    }
    return color;
  }

  getToolbarColor(isDarkMode = false) {
    return isDarkMode ? [255, 255, 255, 0.8] : [0, 0, 0, 0.8]; // Default toolbar
  }

  async onWorkspaceChange(workspace, skipUpdate = false, theme = null) {
    const uuid = workspace.uuid;
    // Use theme from workspace object or passed theme
    let workspaceTheme = theme || workspace.theme;

    await this.foreachWindowAsActive(async (browser) => {
      if (!browser.gZenThemePicker?.promiseInitialized) {
        return;
      }

      if (browser.closing || (await browser.gZenThemePicker?.promiseInitialized)) {
        return;
      }

      if (theme === null) {
        browser.gZenThemePicker.invalidateGradientCache();
      }

      // Do not rebuild if the workspace is not the same as the current one
      const windowWorkspace = await browser.gZenWorkspaces.getActiveWorkspace();
      if (windowWorkspace.uuid !== uuid) {
        return;
      }

      // get the theme from the window
      workspaceTheme = this.fixTheme(theme || windowWorkspace.theme);
      const docElement = browser.document.documentElement;

      if (!skipUpdate) {
        for (const dot of browser.gZenThemePicker.panel.querySelectorAll('.zen-theme-picker-dot')) {
          dot.remove();
        }
      }

      if (theme) {
        const workspaceElement = browser.gZenWorkspaces.workspaceElement(windowWorkspace.uuid);
        if (workspaceElement) {
          workspaceElement.clearThemeStyles();
        }
      }

      if (!skipUpdate) {
        docElement.style.setProperty(
          '--zen-main-browser-background-old',
          docElement.style.getPropertyValue('--zen-main-browser-background')
        );
        docElement.style.setProperty(
          '--zen-main-browser-background-toolbar-old',
          docElement.style.getPropertyValue('--zen-main-browser-background-toolbar')
        );
        docElement.style.setProperty(
          '--zen-background-opacity',
          browser.gZenThemePicker.previousBackgroundOpacity ?? 1
        );
        if (browser.gZenThemePicker.previousBackgroundResolve) {
          browser.gZenThemePicker.previousBackgroundResolve();
        }
        delete browser.gZenThemePicker.previousBackgroundOpacity;
      }

      browser.gZenThemePicker.resetCustomColorList();

      browser.gZenThemePicker.currentOpacity = workspaceTheme.opacity ?? 0.5;
      browser.gZenThemePicker.currentTexture = workspaceTheme.texture ?? 0;

      let dominantColor = this.getMostDominantColor(workspaceTheme.gradientColors);
      const isDefaultTheme = !dominantColor;
      if (isDefaultTheme) {
        dominantColor = this.getNativeAccentColor();
      }

      const opacitySlider = browser.document.getElementById(
        'PanelUI-zen-gradient-generator-opacity'
      );

      {
        let opacity = browser.gZenThemePicker.currentOpacity;
        const svg = browser.gZenThemePicker.sliderWavePath;
        /* eslint-disable no-unused-vars */
        const [_, secondStop, thirdStop] = document.querySelectorAll(
          '#PanelUI-zen-gradient-generator-slider-wave-gradient stop'
        );
        // Opacity can only be between MIN_OPACITY to MAX_OPACITY. Make opacity relative to that range
        if (opacity < MIN_OPACITY) {
          opacity = 0;
        } else if (opacity > MAX_OPACITY) {
          opacity = 1;
        } else {
          opacity = (opacity - MIN_OPACITY) / (MAX_OPACITY - MIN_OPACITY);
        }
        if (isDefaultTheme) {
          opacity = 1; // If it's the default theme, we want the wave to be
        }
        // Since it's sine waves, we can't just set the offset to the opacity, we need to calculate it
        // The offset is the percentage of the wave that is visible, so we need to multiply
        // the opacity by 100 to get the percentage.
        // Set the offset of the stops
        secondStop.setAttribute('offset', `${opacity * 100}%`);
        thirdStop.setAttribute('offset', `${opacity * 100}%`);
        const interpolatedPath = this.#interpolateWavePath(opacity);
        svg.setAttribute('d', interpolatedPath);
        opacitySlider.style.setProperty('--zen-thumb-height', `${40 + opacity * 15}px`);
        opacitySlider.style.setProperty('--zen-thumb-width', `${10 + opacity * 15}px`);
        svg.style.stroke =
          interpolatedPath === this.#linePath
            ? thirdStop.getAttribute('stop-color')
            : 'url(#PanelUI-zen-gradient-generator-slider-wave-gradient)';
      }

      for (const button of browser.document.querySelectorAll(
        '#PanelUI-zen-gradient-generator-color-actions button'
      )) {
        // disable if there are no buttons
        button.disabled =
          workspaceTheme.gradientColors.length === 0 ||
          (button.id === 'PanelUI-zen-gradient-generator-color-add'
            ? workspaceTheme.gradientColors.length >= nsZenThemePicker.MAX_DOTS
            : false) ||
          (button.id === 'PanelUI-zen-gradient-generator-color-toggle-algo'
            ? workspaceTheme.gradientColors.length < 2
            : false);
      }
      const clickToAdd = browser.document.getElementById(
        'PanelUI-zen-gradient-generator-color-click-to-add'
      );
      if (workspaceTheme.gradientColors.length > 0) {
        clickToAdd.setAttribute('hidden', 'true');
      } else {
        clickToAdd.removeAttribute('hidden');
      }

      opacitySlider.value = browser.gZenThemePicker.currentOpacity;
      const textureSelectWrapper = browser.document.getElementById(
        'PanelUI-zen-gradient-generator-texture-wrapper'
      );
      const textureWrapperWidth = textureSelectWrapper.getBoundingClientRect().width;
      // Dont show when hidden
      if (textureWrapperWidth) {
        // rotate and trasnform relative to the wrapper width depending on the texture value
        const textureValue = this.currentTexture;
        const textureHandler = browser.gZenThemePicker._textureHandler;
        const rotation = textureValue * 360 - 90;
        textureHandler.style.transform = `rotate(${rotation + 90}deg)`;
        // add top and left to center the texture handler in relation with textureWrapperWidth
        // based on the rotation
        const top = Math.sin((rotation * Math.PI) / 180) * (textureWrapperWidth / 2) - 6;
        const left = Math.cos((rotation * Math.PI) / 180) * (textureWrapperWidth / 2) - 3;
        textureHandler.style.top = `${textureWrapperWidth / 2 + top}px`;
        textureHandler.style.left = `${textureWrapperWidth / 2 + left}px`;
        // Highlight the 16 buttons based on the texture value
        const buttons = browser.document.querySelectorAll('.zen-theme-picker-texture-dot');
        let i = 4;
        for (const button of buttons) {
          button.classList.toggle('active', i / 16 <= textureValue);
          i++;
          // We start at point 4 because that's the first point that is not in the middle of the texture
          if (i === 16) {
            i = 0;
          }
        }
      }

      const gradient = browser.gZenThemePicker.getGradient(workspaceTheme.gradientColors);
      const gradientToolbar = browser.gZenThemePicker.getGradient(
        workspaceTheme.gradientColors,
        true
      );
      browser.gZenThemePicker.updateNoise(workspaceTheme.texture);

      browser.gZenThemePicker.customColorList.innerHTML = '';
      for (const dot of workspaceTheme.gradientColors) {
        if (dot.isCustom) {
          browser.gZenThemePicker.addColorToCustomList(dot.c);
        }
      }

      docElement.style.setProperty('--zen-main-browser-background-toolbar', gradientToolbar);
      docElement.style.setProperty('--zen-main-browser-background', gradient);
      const isDarkModeWindow = browser.gZenThemePicker.isDarkMode;
      if (isDefaultTheme) {
        docElement.setAttribute('zen-default-theme', 'true');
      } else {
        docElement.removeAttribute('zen-default-theme');
      }
      if (dominantColor) {
        const primaryColor = this.getAccentColorForUI(dominantColor);
        docElement.style.setProperty('--zen-primary-color', primaryColor);

        // Should be set to `this.isLegacyVersion` but for some reason it is set to undefined if we open a private window,
        // so instead get the pref value directly.
        browser.gZenThemePicker.isLegacyVersion =
          Services.prefs.getIntPref('zen.theme.gradient-legacy-version', 1) === 0;

        let isDarkMode = isDarkModeWindow;
        if (!isDefaultTheme && !this.isLegacyVersion) {
          // Check for the primary color
          isDarkMode = browser.gZenThemePicker.shouldBeDarkMode(dominantColor);
          docElement.setAttribute('zen-should-be-dark-mode', isDarkMode);
          browser.gZenThemePicker.panel.removeAttribute('invalidate-controls');
        } else {
          docElement.removeAttribute('zen-should-be-dark-mode');
          if (!this.isLegacyVersion) {
            browser.gZenThemePicker.panel.setAttribute('invalidate-controls', 'true');
          }
        }
        // Set `--toolbox-textcolor` to have a contrast with the primary color
        const textColor = this.getToolbarColor(isDarkMode);
        docElement.style.setProperty(
          '--toolbox-textcolor',
          `rgba(${textColor[0]}, ${textColor[1]}, ${textColor[2]}, ${textColor[3]})`
        );
      }

      if (!skipUpdate) {
        browser.gZenThemePicker.dots = [];
        browser.gZenThemePicker.recalculateDots(workspaceTheme.gradientColors);
      }
    });
  }

  fixTheme(theme) {
    // add a primary color if there isn't one
    if (!theme.gradientColors.find((color) => color.isPrimary) && theme.gradientColors.length > 0) {
      theme.gradientColors[0].isPrimary = true;
    }
    return theme;
  }

  getNativeAccentColor() {
    let accentColor = Services.prefs.getStringPref('zen.theme.accent-color');
    let rgb;
    if (accentColor === 'AccentColor') {
      const rawRgb = window.getComputedStyle(document.getElementById('zen-browser-background'))[
        'color'
      ];
      rgb = rawRgb.match(/\d+/g).map(Number);
      // Match our theme a bit more, since we can't always expect the OS
      // to give us a color matching our theme scheme
      rgb = this.blendColors(
        rgb,
        this.getToolbarModifiedBaseRaw().slice(0, 3),
        this.isDarkMode ? 80 : 50
      );
    } else {
      rgb = this.hexToRgb(accentColor);
    }
    if (this.isDarkMode) {
      // If the theme is dark, we want to use a lighter color
      return this.blendColors(rgb, [0, 0, 0], 40);
    }
    return rgb;
  }

  resetCustomColorList() {
    this.customColorList.innerHTML = '';
  }

  removeCustomColor(event) {
    const target = event.target.closest('.zen-theme-picker-custom-list-item');
    const color = target.getAttribute('data-color');
    const dots = this.panel.querySelectorAll('.zen-theme-picker-dot');
    for (const dot of dots) {
      if (dot.style.getPropertyValue('--zen-theme-picker-dot-color') === color) {
        dot.remove();
        break;
      }
    }
    target.remove();
    this.updateCurrentWorkspace();
  }

  getPrimaryColor(colors) {
    const primaryColor = colors.find((color) => color.isPrimary);
    if (primaryColor) {
      return primaryColor.c;
    }
    if (colors.length === 0) {
      return undefined;
    }
    // Get the middle color
    return colors[Math.floor(colors.length / 2)].c;
  }

  recalculateDots(colors) {
    for (const color of colors) {
      this.createDot(color, true);
    }
  }

  async updateCurrentWorkspace(skipSave = true) {
    this.updated = skipSave;
    const dots = this.panel.querySelectorAll('.zen-theme-picker-dot');
    const colors = Array.from(dots)
      .sort((a, b) => a.getAttribute('data-index') - b.getAttribute('data-index'))
      .map((dot) => {
        const color = dot.style.getPropertyValue('--zen-theme-picker-dot-color');
        const isPrimary = dot.classList.contains('primary');

        if (color === 'undefined') {
          return;
        }
        const isCustom = dot.classList.contains('custom');
        const algorithm = this.useAlgo;
        const position =
          dot.getAttribute('data-position') && JSON.parse(dot.getAttribute('data-position'));
        const type = dot.getAttribute('data-type');
        return {
          c: isCustom ? color : color.match(/\d+/g).map(Number),
          isCustom,
          algorithm,
          isPrimary,
          lightness: this.#currentLightness,
          position,
          type,
        };
      });
    const gradient = nsZenThemePicker.getTheme(colors, this.currentOpacity, this.currentTexture);
    let currentWorkspace = await gZenWorkspaces.getActiveWorkspace();

    if (!skipSave) {
      await ZenWorkspacesStorage.saveWorkspaceTheme(currentWorkspace.uuid, gradient);
      await gZenWorkspaces._propagateWorkspaceData();
      gZenUIManager.showToast('zen-panel-ui-gradient-generator-saved-message');
      currentWorkspace = await gZenWorkspaces.getActiveWorkspace();
    }

    await this.onWorkspaceChange(currentWorkspace, skipSave, skipSave ? gradient : null);
  }

  async handlePanelClose() {
    if (this.updated) {
      await this.updateCurrentWorkspace(false);
    }
    this.uninitThemePicker();
  }

  handlePanelOpen() {
    this.initThemePicker();
    setTimeout(() => {
      this.updateCurrentWorkspace();
    }, 200);
  }

  #interpolateWavePath(progress) {
    const linePath = this.#linePath;
    const sinePath = this.#sinePath;
    const referenceY = 27.3;
    if (this.#sinePoints.length === 0) {
      return progress < 0.5 ? linePath : sinePath;
    }
    if (progress <= 0.001) return linePath;
    if (progress >= 0.999) return sinePath;
    const t = progress;
    let newPathData = '';
    this.#sinePoints.forEach((p) => {
      switch (p.type) {
        case 'M': {
          const interpolatedY = referenceY + (p.y - referenceY) * t;
          newPathData += `M ${p.x} ${interpolatedY} `;
          break;
        }
        case 'C': {
          const y1 = referenceY + (p.y1 - referenceY) * t;
          const y2 = referenceY + (p.y2 - referenceY) * t;
          const y = referenceY + (p.y - referenceY) * t;
          newPathData += `C ${p.x1} ${y1} ${p.x2} ${y2} ${p.x} ${y} `;
          break;
        }
        case 'L':
          newPathData += `L ${p.x} ${p.y} `;
          break;
      }
    });
    return newPathData.trim();
  }

  invalidateGradientCache() {
    this.#gradientsCache = {};
    window.dispatchEvent(new Event('ZenGradientCacheChanged'));
  }

  getGradientForWorkspace(workspace) {
    const uuid = workspace.uuid;
    if (this.#gradientsCache[uuid]) {
      return this.#gradientsCache[uuid];
    }
    const previousOpacity = this.currentOpacity;
    const previousLightness = this.#currentLightness;
    const theme = workspace.theme;
    this.currentOpacity = theme.opacity ?? 0.5;
    this.#currentLightness = theme.lightness ?? 50;
    const gradient = this.getGradient(theme.gradientColors);
    const toolbarGradient = this.getGradient(theme.gradientColors, true);
    let dominantColor = this.getMostDominantColor(theme.gradientColors);
    const isDefaultTheme = !dominantColor;
    if (isDefaultTheme) {
      dominantColor = this.getNativeAccentColor();
    }
    let isDarkMode = this.isDarkMode;
    let isExplicitMode = false;
    if (!isDefaultTheme && !this.isLegacyVersion) {
      // Check for the primary color
      isDarkMode = this.shouldBeDarkMode(dominantColor);
      isExplicitMode = true;
    }
    this.#gradientsCache[uuid] = {
      gradient,
      toolbarGradient,
      grain: theme.texture ?? 0,
      isDarkMode,
      isExplicitMode,
      toolbarColor: this.getToolbarColor(isDarkMode),
      primaryColor: this.getAccentColorForUI(dominantColor),
    };
    this.currentOpacity = previousOpacity;
    this.#currentLightness = previousLightness;
    return this.#gradientsCache[uuid];
  }
}
